/**
 * 
 */
package x.java.tea;

import java.io.IOException;
import java.io.OutputStream;

/**
 * This class uses a underlying output stream as its basic sink of data, but
 * possibly transforming the data along the way.
 * 
 * The class itself overrides all methods of OutputStream and delegates all
 * requests to the underlying output stream after passing through a state
 * machine. If a marker is recognized, the injector will be invoked and its data
 * stream will be merged into the output stream.
 * 
 * @author qiangli
 * 
 */
public class FilterOutputStream extends OutputStream {
	protected OutputStream out;
	
	protected Injector injector;

	private State state;

	private byte[] buf;

	/**
	 * 
	 */
	public FilterOutputStream() {
		this(null, null, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}
	
	public FilterOutputStream(OutputStream out) {
		this(out, null, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}
	
	public FilterOutputStream(OutputStream out, Injector injector) {
		this(out, injector, Constant.ID_PATTERN, Constant.MAX_ID_LEN,
				Constant.MAX_BUF_SIZE);
	}

	public FilterOutputStream(OutputStream out, Injector injector,
			String idpattern, int maxidlen, int maxbufsize) {
		this.out = out;
		this.injector = injector;
		this.buf = new byte[maxbufsize];
		this.state = new State(idpattern.getBytes(), new byte[maxidlen]);
	}

	public void setOutputStream(OutputStream out) {
		this.out = out;
	}

	public void setInjector(Injector injector) {
		this.injector = injector;
	}


	private void inject(String id) throws IOException {
		injector.init(id);
		while (true) {
			int cnt = injector.inject(buf);
			if (cnt == -1) {
				return;
			}
			if (cnt == 0) {
				continue;
			}
			out.write(buf, 0, cnt);
		}
	}

	protected void filter(int b) throws IOException {
		int s = state.filter(b);
		switch (s) {
		case State.ABORT:
			out.write(state.getStart());
			out.write(b);
			break;
		case State.LEAVE:
			inject(new String(state.getId()));
			break;
		case State.NORMAL:
			out.write(b);
			break;
		case State.START:
		case State.ENTER:
		default:
			// skip
		}
	}
	
	@Override
	public void write(int b) throws IOException {
		filter(b);
	}

	@Override
	public void write(byte[] b, int off, int len) throws IOException {
		len += off;
		for (int i = off; i < len; i++) {
			write(b[i]);
		}
	}

	@Override
	public void write(byte[] b) throws IOException {
		write(b, 0, b.length);
	}
}
